Описание проекта:
Доступен датасет с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года. Информация, размещённая в сервисе Яндекс Бизнес, могла быть добавлена пользователями или найдена в общедоступных источниках. Необходимо подготовить исследование рынка заведений общественного питания Москвы, проанализировать его особенности и презентовать полученные результаты. Эти данные должны помочь инвесторам в выборе подходящего типа и расположения нового заведения, а также его меню и цен. Инвесторы помимо наиболее выгодного для открытия типа заведения хотели бы открыть как минимум одну кофейню, поэтому помимо общих рекомендаций по открытию нового заведения необходимо дать рекомендации по выбору места для открытия кофейни.
Цели исследования:
Рабочие файлы:
moscow_places.csv - датасет с данными о заведениях общественного питания Москвыadmin_level_geomap.geojson - файл с границами районов МосквыИнструменты:
Ход исследования:
Исследование пройдет в 3 этапа:
Файл moscow_places.csv:
name — название заведения;category — категория заведения, например «кафе», «пиццерия» или «кофейня»;address — адрес заведения;district — административный район, в котором находится заведение, например Центральный административный округ;hours — информация о днях и часах работы;lat — широта географической точки, в которой находится заведение;lng — долгота географической точки, в которой находится заведение;rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»:middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»:chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки):seats — количество посадочных мест.Импортируем нужные для работы библиотеки
# библиотеки анализа данных
import pandas as pd
import numpy as np
# библиотеки визуализации
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
from plotly import graph_objects as go
import folium
from folium.features import CustomIcon
from folium.features import DivIcon
from folium.plugins import MarkerCluster
# прочие библиотеки
import json
import requests
import warnings
# отключим UserWarning
warnings.filterwarnings('ignore')
Загружаем данные.
Внимание: датасеты защищены авторским правом Яндекс Практикума и не приложены к проекту.
# функция для открытия csv-файла с запасным путем
def open_csv(path_1,name_file_1,path_2,name_file_2):
way_1 = path_1 + name_file_1
way_2 = path_2 + name_file_2
try:
df = pd.read_csv(way_1)
print('Файл \033[1m' + name_file_1 + '\033[0m успешно прочитан по локальному пути')
except:
try:
df = pd.read_csv(way_2)
print('Файл \033[1m' + name_file_2 + '\033[0m успешно прочитан по сетевому пути')
except:
return print('Ошибка чтения файла \033[1m' + name_file_2 + '\033[0m')
return df
# функция для открытия json-файла с запасным путем
def open_json(path_1,name_file_1,path_2,name_file_2):
way_1 = path_1 + name_file_1
way_2 = path_2 + name_file_2
try:
with open(way_1,encoding='utf-8') as f:
state_geo = json.load(f)
print('Файл \033[1m' + name_file_1 + '\033[0m успешно прочитан по локальному пути')
except:
try:
response = requests.get(way_2)
state_geo = json.loads(response.text)
print('Файл \033[1m' + name_file_2 + '\033[0m успешно прочитан по сетевому пути')
except:
return print('Ошибка чтения файла \033[1m' + name_file_2 + '\033[0m')
return state_geo
# загружаем csv-файл с данными о заведениях
path_local = ''
name_file_local = 'moscow_places.csv'
path_web = ''
name_file_web = 'moscow_places.csv'
data = open_csv(path_local,
name_file_local,
path_web,
name_file_web)
# загружаем JSON-файл с границами округов Москвы
path_local = ''
name_file_local = 'admin_level_geomap.geojson'
path_web = '' # защищено авторским правом
name_file_web = 'admin_level_geomap.geojson'
state_geo = open_json(path_local,
name_file_local,
path_web,
name_file_web)
Файл moscow_places.csv успешно прочитан по локальному пути Файл admin_level_geomap.geojson успешно прочитан по локальному пути
Делаем обзор данных
# функция для обзора столбцов датафрейма
def data_info(data_import):
# создаем столбцы с нужными данными и склеиваем их в один датафрейм
data_info = (pd.DataFrame(data_import.dtypes,
columns=['Тип столбца'])
.join(pd.DataFrame(data_import.count(),
columns=['Ненулевых значений']))
.join(pd.DataFrame(data_import.nunique(),
columns=['Уникальных значений']))
.join(pd.DataFrame(data_import.isna().sum(),
columns=['Пропусков']))
.join(pd.DataFrame(data_import.isna().sum()/data_import.shape[0],
columns=['Процент пропусков'])))
# выводим полученный датафрейм
display(data_info.style.format({'Процент пропусков': '{:.0%}'}))
# функция для обзора датафрейма, вывода первых строк и гистограмм
def data_review (data_import):
# выведем общую обзорную информацию о датафрейме
display(pd.DataFrame(data=[data_import.shape[0],
data_import.shape[1],
data_import.duplicated().sum()],
columns=['Всего в датасете'],
index=['Строк','Столбцов','Явных дубликатов']))
# выводем информацию о столбцах датафрейма
data_info(data_import)
# выводем первые 5 строк
print(display(data_import.head()))
# выведем гистограммы по числовым столбцам
with plt.style.context('seaborn-darkgrid'):
data.hist(figsize=(10, 10),ec='black')
plt.show()
# делаем обзор датафрейма
data_review(data)
| Всего в датасете | |
|---|---|
| Строк | 8406 |
| Столбцов | 14 |
| Явных дубликатов | 0 |
| Тип столбца | Ненулевых значений | Уникальных значений | Пропусков | Процент пропусков | |
|---|---|---|---|---|---|
| name | object | 8406 | 5614 | 0 | 0% |
| category | object | 8406 | 8 | 0 | 0% |
| address | object | 8406 | 5753 | 0 | 0% |
| district | object | 8406 | 9 | 0 | 0% |
| hours | object | 7870 | 1307 | 536 | 6% |
| lat | float64 | 8406 | 8209 | 0 | 0% |
| lng | float64 | 8406 | 8258 | 0 | 0% |
| rating | float64 | 8406 | 41 | 0 | 0% |
| price | object | 3315 | 4 | 5091 | 61% |
| avg_bill | object | 3816 | 897 | 4590 | 55% |
| middle_avg_bill | float64 | 3149 | 230 | 5257 | 63% |
| middle_coffee_cup | float64 | 535 | 96 | 7871 | 94% |
| chain | int64 | 8406 | 2 | 0 | 0% |
| seats | float64 | 4795 | 229 | 3611 | 43% |
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 |
None
Выводы о проблемах с данными:
Пропуски
hourspriceavg_billmiddle_avg_billmiddle_coffee_cupseatsДубликаты
name, address, hours, avg_bill слишком много уникальных значений, поэтому проверить данные столбцы на неявные дубликаты не представляется возможным. На неявные дубликаты можно проверить только эти столбцы:categorydistrictpriceНазвания столбцов
Типы данных
seats стоит поменять на целочисленный. Количество посадочных мест по определению не может быть дробным числом. Однако, учитывая там наличие пропусков, лучше оставить вещественный тип, иначе это можеть усложнить некоторые операции со столбцом.chain всего два уникальных значения, тип данных int для него избыточен, его можно поменять на более компактный тип bool.Выбросы
# меняем тип столбца chain на bool
data['chain'] = data['chain'].astype('bool')
# проверяем на дубликаты столбец category
data['category'].sort_values().unique()
array(['бар,паб', 'булочная', 'быстрое питание', 'кафе', 'кофейня',
'пиццерия', 'ресторан', 'столовая'], dtype=object)
# проверяем на дубликаты столбец district
data['district'].sort_values().unique()
array(['Восточный административный округ',
'Западный административный округ',
'Северный административный округ',
'Северо-Восточный административный округ',
'Северо-Западный административный округ',
'Центральный административный округ',
'Юго-Восточный административный округ',
'Юго-Западный административный округ',
'Южный административный округ'], dtype=object)
# проверяем на дубликаты столбец price
data['price'].sort_values().unique()
array(['высокие', 'выше среднего', 'низкие', 'средние', nan], dtype=object)
Вывод: Неявных дубликатов нет. Их обработка не требуется.
Во всех столбцах c пропусками кроме столбца hours очень много пропусков. Удаление строк с пропусками приведет к значительной потере данных, замена значений на медиану/моду может привести к значительному смещению распределения данных и искажению дальнейших расчетов. В связи с этим пропуски в числовых столбцах проигнорируем и оставим как есть. Пропуски в категориальных столбцах hours,price,avg_bill можно заменить заглушкой unknown.
# заполняем пропуски в столбцах
data['hours'] = data['hours'].fillna('unknown')
data['price'] = data['price'].fillna('unknown')
data['avg_bill'] = data['avg_bill'].fillna('unknown')
Посмотрим, есть ли выбросы в данных.
# функция для построения графика плотности распределения,
# точечного графика-полоски, боксплота и вывода основых статистик
def describe_func(df,column):
# выводим основные статистики
print('********************')
df_desc = pd.DataFrame(df[column].describe())
quant_1 = df[column].quantile([0.25])[0.25]
quant_3 = df[column].quantile([0.75])[0.75]
IQR = quant_3 - quant_1
df_desc.loc['75%+1.5*IQR'] = quant_3 + 1.5 * IQR
df_desc.loc['25%-1.5*IQR'] = quant_1 - 1.5 * IQR
display(df_desc.round(2))
# задаем размер подложки для графиков
fig = plt.subplots(figsize=(20,6))
# задаем единый стиль графиков
sns.set_style('darkgrid')
# строим график плотности распределения
ax1 = plt.subplot(1, 3, 1)
sns.violinplot(y=column, data=df,ax=ax1)
ax1.set_title('График плотности распределения {}'.format(column))
ax1.set_ylabel('')
# строим точечный график-полоску
ax2 = plt.subplot(1, 3, 2, sharey=ax1)
sns.stripplot(y=column, data=df, ax=ax2, color='#3274a1')
ax2.set_title('Точечный график распределения {}'.format(column))
ax2.set_ylabel('')
# строим боксплот
ax3 = plt.subplot(1, 3, 3, sharey=ax1)
sns.boxplot(y=column, data=df, ax=ax3)
ax3.set_title('Боксплот {}'.format(column))
ax3.set_ylabel('')
# выводим графики
plt.show();
# задаем список имен числовых столбцов
list_col = ['lat',
'lng',
'rating',
'middle_avg_bill',
'middle_coffee_cup',
'seats']
# выводим обзор распределений
for name_col in list_col:
describe_func(data,name_col)
********************
| lat | |
|---|---|
| count | 8406.00 |
| mean | 55.75 |
| std | 0.07 |
| min | 55.57 |
| 25% | 55.71 |
| 50% | 55.75 |
| 75% | 55.80 |
| max | 55.93 |
| 75%+1.5*IQR | 55.93 |
| 25%-1.5*IQR | 55.57 |
********************
| lng | |
|---|---|
| count | 8406.00 |
| mean | 37.61 |
| std | 0.10 |
| min | 37.36 |
| 25% | 37.54 |
| 50% | 37.61 |
| 75% | 37.66 |
| max | 37.87 |
| 75%+1.5*IQR | 37.85 |
| 25%-1.5*IQR | 37.35 |
********************
| rating | |
|---|---|
| count | 8406.00 |
| mean | 4.23 |
| std | 0.47 |
| min | 1.00 |
| 25% | 4.10 |
| 50% | 4.30 |
| 75% | 4.40 |
| max | 5.00 |
| 75%+1.5*IQR | 4.85 |
| 25%-1.5*IQR | 3.65 |
********************
| middle_avg_bill | |
|---|---|
| count | 3149.00 |
| mean | 958.05 |
| std | 1009.73 |
| min | 0.00 |
| 25% | 375.00 |
| 50% | 750.00 |
| 75% | 1250.00 |
| max | 35000.00 |
| 75%+1.5*IQR | 2562.50 |
| 25%-1.5*IQR | -937.50 |
********************
| middle_coffee_cup | |
|---|---|
| count | 535.00 |
| mean | 174.72 |
| std | 88.95 |
| min | 60.00 |
| 25% | 124.50 |
| 50% | 169.00 |
| 75% | 225.00 |
| max | 1568.00 |
| 75%+1.5*IQR | 375.75 |
| 25%-1.5*IQR | -26.25 |
********************
| seats | |
|---|---|
| count | 4795.00 |
| mean | 108.42 |
| std | 122.83 |
| min | 0.00 |
| 25% | 40.00 |
| 50% | 75.00 |
| 75% | 140.00 |
| max | 1288.00 |
| 75%+1.5*IQR | 290.00 |
| 25%-1.5*IQR | -110.00 |
Вывод по итогам анализа выбросов:
Cудя по визуализации распределений и вывода основных статистик, нет оснований считать, что в столбцах lat, lng есть существенные выбросы. Однако выбросы определенно есть в столбцах:
middle_avg_bill middle_coffee_cup seatsratingУстранять выбросы в данных столбцах не будем, так как выбросы в них не похожи на явные ошибки. Выбросы в middle_avg_bill , middle_coffee_cup могут говорить об элитных заведениях с высокими ценами. Выбросы в seats — об аномально больших заведениях. Выбросы в rating — об аномально плохих заведениях.
Создадим столбец street с названиями улиц из столбца с адресом.
# создаем функцию для получения названия улицы
def street_search(string):
list =string.split(", ")
return list[1]
# создаем столбец с названием улицы
data['street'] = data['address'].apply(street_search)
Создадим столбец is_24/7 с обозначением, что заведение работает ежедневно и круглосуточно (24/7): логическое значение True — если заведение работает ежедневно и круглосуточно; логическое значение False — в противоположном случае.
# посмотрим на уникальные значения столбца hours
data['hours'].value_counts()
ежедневно, 10:00–22:00 759
ежедневно, круглосуточно 730
unknown 536
ежедневно, 11:00–23:00 396
ежедневно, 10:00–23:00 310
...
пн-пт 17:00–03:00; сб,вс 17:00–05:00 1
пн,вт 09:00–21:00; ср-пт 09:00–22:00; сб 10:00–22:00; вс 10:00–21:00 1
пн-пт 12:00–01:00 1
пн-пт 10:30–21:30; сб,вс 10:30–22:30 1
пн-сб 10:30–21:30 1
Name: hours, Length: 1308, dtype: int64
Ежедневность работы определяется словом "ежедневно", круглосуточность — словом "круглосуточно". Создадим столбец с флагом 24/7.
# создадим функцию для определения флага 24/7
def word_search(string):
if 'круглосуточно' in string.lower():
if 'ежедневно' in string.lower():
return True
else:
return False
else: return False
# создаем столбец с флагом 24/7
data['is_24/7'] = data['hours'].apply(word_search)
Создадим еще один столбец с кратким названием района для удобства дальнейшего анализа данных по районам.
# задаем список полных названий районов
wrong_values = ['Северный административный округ',
'Северо-Восточный административный округ',
'Северо-Западный административный округ',
'Западный административный округ',
'Центральный административный округ',
'Восточный административный округ',
'Юго-Восточный административный округ',
'Южный административный округ',
'Юго-Западный административный округ']
# задаем список кратких названий районов
correct_values = ['САО',
'СВАО',
'СЗАО',
'ЗАО',
'ЦАО',
'ВАО',
'ЮВАО',
'ЮАО',
'ЮЗАО']
# создаем столбец с краткими названиями
data['dstr'] = data['district']
for index in range(len(wrong_values)):
data['dstr'] = data['dstr'].replace(wrong_values[index], correct_values[index])
# для проверки выведем уникальные сочетания новых и старых названий
data.groupby(['district','dstr'])['name'].count().index
MultiIndex([( 'Восточный административный округ', 'ВАО'),
( 'Западный административный округ', 'ЗАО'),
( 'Северный административный округ', 'САО'),
('Северо-Восточный административный округ', 'СВАО'),
( 'Северо-Западный административный округ', 'СЗАО'),
( 'Центральный административный округ', 'ЦАО'),
( 'Юго-Восточный административный округ', 'ЮВАО'),
( 'Юго-Западный административный округ', 'ЮЗАО'),
( 'Южный административный округ', 'ЮАО')],
names=['district', 'dstr'])
Добавим площади округов в квадратный километрах, они понадобятся нам в дальнейшем. Площади округов взяты из Википедии
# задаем функцию для добавления площади района
def area_value(x):
if x == 'Восточный административный округ':
return 154.84
elif x == 'Западный административный округ':
return 153.03
elif x == 'Северный административный округ':
return 113.73
elif x == 'Северо-Восточный административный округ':
return 101.88
elif x == 'Северо-Западный административный округ':
return 93.28
elif x == 'Центральный административный округ':
return 66.18
elif x == 'Юго-Восточный административный округ':
return 117.56
elif x == 'Юго-Западный административный округ':
return 111.36
elif x == 'Южный административный округ':
return 131.77
else:
return 'error'
# добавляем площади в новый столбец
data['area'] = data['district'].apply(area_value)
Поменяем в столбце category название категории "быстрое питание" на более компактное.
data['category'] = data['category'].replace('быстрое питание', 'быстр.пит.')
# выводим информацию о столбцах
data_info(data)
# выводим первые 5 строк датафрейма
data.head()
| Тип столбца | Ненулевых значений | Уникальных значений | Пропусков | Процент пропусков | |
|---|---|---|---|---|---|
| name | object | 8406 | 5614 | 0 | 0% |
| category | object | 8406 | 8 | 0 | 0% |
| address | object | 8406 | 5753 | 0 | 0% |
| district | object | 8406 | 9 | 0 | 0% |
| hours | object | 8406 | 1308 | 0 | 0% |
| lat | float64 | 8406 | 8209 | 0 | 0% |
| lng | float64 | 8406 | 8258 | 0 | 0% |
| rating | float64 | 8406 | 41 | 0 | 0% |
| price | object | 8406 | 5 | 0 | 0% |
| avg_bill | object | 8406 | 898 | 0 | 0% |
| middle_avg_bill | float64 | 3149 | 230 | 5257 | 63% |
| middle_coffee_cup | float64 | 535 | 96 | 7871 | 94% |
| chain | bool | 8406 | 2 | 0 | 0% |
| seats | float64 | 4795 | 229 | 3611 | 43% |
| street | object | 8406 | 1448 | 0 | 0% |
| is_24/7 | bool | 8406 | 2 | 0 | 0% |
| dstr | object | 8406 | 9 | 0 | 0% |
| area | float64 | 8406 | 9 | 0 | 0% |
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | is_24/7 | dstr | area | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | unknown | unknown | NaN | NaN | False | NaN | улица Дыбенко | False | САО | 113.73 |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | False | 4.0 | улица Дыбенко | False | САО | 113.73 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00... | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | False | 45.0 | Клязьминская улица | False | САО | 113.73 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | unknown | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | False | NaN | улица Маршала Федоренко | False | САО | 113.73 |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | True | 148.0 | Правобережная улица | False | САО | 113.73 |
Что было cделано на этапе предобработки:
chain изменили на boolunknown в столбцах: hourspriceavg_bill street с названием улицыis_24/7 c флагом 24/7dstr c кратким названием районаcategory на более компактноеarea с площадью округовПосмотрим, какие категории заведений представлены в данных.
# задаем единый стиль для графиков
sns.set_style('dark')
# готовим данные для графика
df = pd.DataFrame(data['category'].value_counts()).reset_index()
# строим график
fig = go.Figure(data=[go.Pie(labels=df['index'],
values=df['category'],
pull = [0.1, 0])])
# настраиваем параметры графика
fig.update_layout(title={'text': 'Число заведений по категориям','x': 0.6},
width=800,
height=600,
annotations=[dict(x=1,
y=1,
text='Тип заведения',
showarrow=False)])
# выводим график
fig.show()
# готовим данные для графика
df = (data.groupby('category',as_index = False)['name'].count().
rename(columns={'name': 'points_count'}).sort_values(by='points_count',ascending=False))
# строим график
fig = px.bar(df,
y='category',
x='points_count',
text='points_count',
color='category')
# настраиваем параметры графика
fig.update_layout(title={'text': 'Число заведений по категориям','x': 0.5},
xaxis_title='',
yaxis_title='',
width=850,
height=600)
fig.update_xaxes(tickvals=[])
fig.update_xaxes(title_text='Число заведений')
fig.update_yaxes(title_text='Категория заведения')
# выводим график
fig.show()
Вывод:
Посмотрим на распределение посадочных мест по категориям заведений. В качестве наиболее характерного значения возьмем медиану, так как в посадочных местах есть выбросы.
# готовим данные для графика
df = data.groupby('category',as_index = False)['seats'].median().sort_values(by='seats')
# строим график
fig = px.bar(df,
y='category',
x='seats',
text='seats')
# настраиваем параметры графика
fig.update_layout(title={'text': 'Медианное значение посадочных мест по категориям заведений','x': 0.5},
xaxis_title='',
yaxis_title='',
width=850,
height=600)
fig.update_xaxes(tickvals=[])
fig.update_xaxes(title_text='Число посадочных мест')
fig.update_yaxes(title_text='Категории заведений')
# выводим график
fig.show()
Выводы:
Посмотрим на соотношение сетевых и несетевых заведений.
# готовим данные для графика
df = data.groupby('chain', as_index = False)['name'].count()
df = df.rename(columns={'name': 'chain_count'})
df['chain_perc'] = df['chain_count'] / df['chain_count'].sum() * 100
df['all'] = ''
df['chain_new_name'] = df['chain']
df['chain_new_name'] = df['chain_new_name'].replace(True,'Сетевое')
df['chain_new_name'] = df['chain_new_name'].replace(False,'Несетевое')
# строим график
fig = go.Figure(data=[go.Pie(labels=df['chain_new_name'],
values=df['chain_perc'],
pull = [0.1, 0])])
# настраиваем параметры графика
fig.update_layout(title={'text': 'Отношение сетевых и несетевых заведений','x': 0.6},
width=800,
height=600,
annotations=[dict(x=1,
y=1,
text='Тип заведения',
showarrow=False)])
# выводим график
fig.show()
Вывод:
Посмотрим какие категории заведений чаще являются сетевыми.
# готовим данные для графика
df = data.groupby(['chain','category'], as_index = False)['name'].count()
df = df.rename(columns={'name': 'chain_count'})
df['all_categ'] = df.groupby(['category'])['chain_count'].transform(sum)
df['chain_perc'] = round(df['chain_count'] / df['all_categ'],4) * 100
df['chain_new_name'] = df['chain']
df['chain_new_name'] = df['chain_new_name'].replace(True,'Сетевое')
df['chain_new_name'] = df['chain_new_name'].replace(False,'Несетевое')
df=df.sort_values(by='chain_perc')
# строим график
fig = px.bar(df,
x='chain_perc',
y='category',
color='chain_new_name',
text = round(df['chain_perc']))
# настраиваем параметры графика
fig.update_layout(title={'text': 'Отношение сетевых и несетевых заведений '
'по категориям заведений','x': 0.485},
legend_title='Тип заведения',
xaxis_title='Процент',
yaxis_title='Категории заведений',
width=900,
height=400,
plot_bgcolor="white")
# выводим график
fig.show()
Выводы:
Выведем топ-15 сетевых заведений с наибольших количеством точек.
Для этого возьмем из датасета только сетевые заведения и сгруппируем их по названию и категории. Только по названию группировать не стоит, потому что названия заведений разных категорий могут повторяться, но вряд ли среди сетевых заведений в одном городе разные заведения одной категории будут называться одинаково. Хотя, конечно, одному и тому же сетевому заведению может быть установлена разная категория для разных его точек и это исказит подсчет, если группировать по имени и категории одновременно. В данном случае проверить корректность установки категорий сложно, поэтому предположим, что категории заведений в датасете указаны верно и в основном для одних и тех же сетевых заведений однообразно.
# строим график количества точек для топ-15 сетевых заведений
# в разбивке по названиям заведений с выделением типа категории заведения
# готовим данные для графика
df = (data[data['chain']]
.groupby(['name','category'], as_index = False)['address'].count()
.rename(columns={'address': 'points_count'})
.sort_values(by='points_count',ascending=False).head(15)
.sort_values(by='points_count'))
# строим график
fig = px.bar(df,
y='name',
x='points_count',
text='points_count')
# настраиваем параметры графика
fig.update_layout(title={'text': 'ТОП-15 сетевых заведений по количеству точек','x': 0.55},
legend_title='Категория заведения',
xaxis_title='Количество точек',
yaxis_title='Имя заведения',
width=900,
height=600)
fig.update_xaxes(tickvals=[])
# выводим график
fig.show()
# строим график количества заведений для топ-15 сетевых заведений
# в разбивке по категориям заведений
# готовим данные для графика
df1 = (df.groupby('category', as_index = False)['name'].count().sort_values(by='name', ascending=False)
.rename(columns={'name': 'name_count'}))
# строим график
fig = px.bar(df1,
y='category',
x='name_count',
text='name_count',
color='category')
# настраиваем параметры графика
fig.update_layout(title={'text': 'Количество заведений среди ТОП-15 заведений','x': 0.5},
legend_title='Категория заведения',
xaxis_title='Количество заведений',
yaxis_title='Категории заведений',
width=800,
height=300)
fig.update_xaxes(tickvals=[])
# выводим график
fig.show()
# строим график количества точек для топ-15 сетевых заведений
# в разбивке по категориям заведений
# готовим данные для графика
df2 = df.groupby('category', as_index = False)['points_count'].sum().sort_values(by='points_count', ascending=False)
# строим график
fig = px.bar(df2,
y='category',
x='points_count',
text='points_count',
color='category')
# настраиваем параметры графика
fig.update_layout(title={'text': 'Количество точек для ТОП-15 заведений','x': 0.5},
legend_title='Категория заведения',
xaxis_title='Количество точек',
yaxis_title='Категории заведений',
width=800,
height=300)
fig.update_xaxes(tickvals=[])
# выводим график
fig.show()
Выводы:
Посмотрим распределение заведений и их категорий по районам.
# строим столбчатую диаграмму
# распределения числа заведений по районам
# готовим данные для графика
df = (data.groupby('dstr', as_index = False)['name'].count()
.rename(columns={'name': 'points_count'})
.sort_values(by='points_count'))
# строим график
fig = px.bar(df,
y='dstr',
x='points_count',
text='points_count')
# настраиваем параметры графика
fig.update_layout(title={'text': 'Количество заведений по районам','x': 0.5},
xaxis_title='Количество заведений',
yaxis_title='Район',
width=900,
height=600)
fig.update_xaxes(tickvals=[])
# выводим график
fig.show()
# строим столбчатую диаграмму
# процентного распределения категорий заведений по районам
# готовим данные для графика
df = (data.groupby(['category','dstr'], as_index = False)['name'].count()
.rename(columns={'name': 'points_count_categ_dstr'}))
df['all_points_dstr'] = df.groupby('dstr')['points_count_categ_dstr'].transform(sum)
df['perc_categ'] = df['points_count_categ_dstr'] / df['all_points_dstr'] * 100
df['all_points_categ'] = df.groupby('category')['points_count_categ_dstr'].transform(sum)
df = df.sort_values(by=['all_points_dstr','all_points_categ'])
# строим график
fig = px.bar(df,
x='perc_categ',
y='dstr',
color='category',
text = round(df['perc_categ']))
# настраиваем параметры графика
fig.update_layout(title={'text': 'Соотношение количества заведений в разных районах по категориям','x': 0.45},
legend_title='Категория заведения',
xaxis_title='Процент',
yaxis_title='Район',
width=950,
height=400,
plot_bgcolor="white")
fig.update_traces(textposition="inside")
# выводим график
fig.show()
# строим столбчатую диаграмму
# процентного распределения количества заведений в районах по категориям заведений
# готовим данные для графика
df = (data.groupby(['category','dstr'], as_index = False)['name'].count()
.rename(columns={'name': 'points_count_categ_dstr'}))
df['all_points_categ'] = df.groupby('category')['points_count_categ_dstr'].transform(sum)
df['perc_dstr'] = df['points_count_categ_dstr'] / df['all_points_categ'] * 100
df['all_points_dstr'] = df.groupby('dstr')['points_count_categ_dstr'].transform(sum)
df = df.sort_values(by=['all_points_categ','all_points_dstr'])
# строим график
fig = px.bar(df,
x='perc_dstr',
y='category',
color='dstr',
text = round(df['perc_dstr']))
# настраиваем параметры графика
fig.update_layout(title={'text': 'Соотношение количества заведений в разных категориях по районам','x': 0.46},
legend_title='Категория заведения',
xaxis_title='Процент',
yaxis_title='Категории заведений',
width=950,
height=400,
plot_bgcolor="white")
fig.update_traces(textposition="inside")
# выводим график
fig.show()
# строим хитмеп
# количества заведений по районам и категориями
# готовим данные для хитмепа
data_pivot = data.pivot_table(index = 'dstr', columns = 'category',
values = 'name', aggfunc = 'count')
data_pivot['sum_all_1'] = data_pivot.sum(axis=1)
data_pivot=data_pivot.sort_values(by='sum_all_1')
data_pivot=data_pivot.drop('sum_all_1', axis=1)
data_pivot=data_pivot.T
data_pivot['sum_all_2'] = data_pivot.sum(axis=1)
data_pivot=data_pivot.sort_values(by='sum_all_2',ascending=False)
data_pivot=data_pivot.drop('sum_all_2', axis=1)
# строим хитмеп
plt.figure(figsize=(10, 5))
ax = sns.heatmap(
data_pivot,
annot=True,
fmt='.0f',
cmap= 'coolwarm',
linecolor='black',
linewidths=2)
# настраиваем параметры хитмепа
plt.xlabel('x_label')
ax.xaxis.set_ticks_position('top')
plt.title('Количество заведений по категориям и районам\n')
plt.xlabel('')
plt.ylabel('')
# выводим хитмеп
plt.show()
Выводы:
Посмотрим дополнительно на число заведений на 1 кв.км. по районам
# создадим функцию для построения хороплета
def map_chor(df_in,
column_1,
column_2,
legend_name,
nan_fill_opacity = 0,
markers_on=False,
clusters_on=False,
fill_opacity=0.8,
zoom_start=10):
# выводим таблицу
if (markers_on==False) and (clusters_on==False):
display(df_in[[column_1, column_2]].sort_values(by=column_2,ascending=False).reset_index(drop=True))
# задаем широту и долготу центра карты
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту
m = folium.Map(location=[moscow_lat, moscow_lng], zoom_start=zoom_start)
# если нужно нанести маркеры
if markers_on == True:
def create_marker(row):
folium.Marker([row['lat'], row['lng']],
popup=f"{row['name']} {row['rating']}",
fill_color='blue',
color="blue",
radius = 2, fill_opacity = 1
).add_to(m)
df.apply(create_marker, axis=1)
# создаем пустой столбец для отображения границ округов
df[column_2] = 0
# если нужно создать кластеры
if clusters_on == True:
marker_cluster = MarkerCluster().add_to(m)
def create_clusters(row):
folium.Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['rating']}",
).add_to(marker_cluster)
data.apply(create_clusters, axis=1)
# создаем пустой столбец для отображения границ округов
df[column_2] = 0
# создаём хороплет
folium.Choropleth(
geo_data=state_geo,
data=df_in,
columns=[column_1, column_2],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=fill_opacity,
legend_name=legend_name,
nan_fill_opacity=nan_fill_opacity,
highlight=True
).add_to(m)
# наносим на карту названия округов
folium.map.Marker(
[moscow_lat+0.02, moscow_lng-0.02],
icon=DivIcon(html='<div style="font-size: 15pt">ЦАО</div>')).add_to(m)
folium.map.Marker(
[moscow_lat+0.06, moscow_lng+0.1],
icon=DivIcon(html='<div style="font-size: 15pt">ВАО</div>')).add_to(m)
folium.map.Marker(
[moscow_lat+0.12, moscow_lng-0.015],
icon=DivIcon(html='<div style="font-size: 15pt">СВАО</div>')).add_to(m)
folium.map.Marker(
[moscow_lat+0.11, moscow_lng-0.13],
icon=DivIcon(html='<div style="font-size: 15pt">САО</div>')).add_to(m)
folium.map.Marker(
[moscow_lat+0.06, moscow_lng-0.2],
icon=DivIcon(html='<div style="font-size: 15pt">СЗАО</div>')).add_to(m)
folium.map.Marker(
[moscow_lat-0.03, moscow_lng-0.16],
icon=DivIcon(html='<div style="font-size: 15pt">ЗАО</div>')).add_to(m)
folium.map.Marker(
[moscow_lat-0.11, moscow_lng-0.13],
icon=DivIcon(html='<div style="font-size: 15pt">ЮЗАО</div>')).add_to(m)
folium.map.Marker(
[moscow_lat-0.12, moscow_lng],
icon=DivIcon(html='<div style="font-size: 15pt">ЮАО</div>')).add_to(m)
folium.map.Marker(
[moscow_lat-0.04, moscow_lng+0.11],
icon=DivIcon(html='<div style="font-size: 15pt">ЮВАО</div>')).add_to(m)
# выводим карту
return m
# считаем число заведений на 1 кв.км. по районам
df = data.groupby(['district','area'], as_index = False)['name'].count()
df['count_per_area'] = round(df['name'] / df['area'],2)
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='count_per_area',
legend_name='Число заведений на 1 кв.км.')
| district | count_per_area | |
|---|---|---|
| 0 | Центральный административный округ | 33.88 |
| 1 | Северо-Восточный административный округ | 8.75 |
| 2 | Северный административный округ | 7.91 |
| 3 | Южный административный округ | 6.77 |
| 4 | Юго-Западный административный округ | 6.37 |
| 5 | Юго-Восточный административный округ | 6.07 |
| 6 | Западный административный округ | 5.56 |
| 7 | Восточный административный округ | 5.15 |
| 8 | Северо-Западный административный округ | 4.38 |
Вывод:
Чтобы визуально лучше сравнить разницу в числе заведений на 1 кв.км по районам, посмотрим дополнительно на хороплет без ЦАО.
# считаем число заведений на 1 кв.км. по районам без ЦАО
df = (data[data['district'] != 'Центральный административный округ']
.groupby(['district','area'], as_index = False)['name'].count())
df['count_per_area'] = round(df['name'] / df['area'],2)
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='count_per_area',
legend_name='Число заведений на 1 кв.км.',
nan_fill_opacity=0.2)
| district | count_per_area | |
|---|---|---|
| 0 | Северо-Восточный административный округ | 8.75 |
| 1 | Северный административный округ | 7.91 |
| 2 | Южный административный округ | 6.77 |
| 3 | Юго-Западный административный округ | 6.37 |
| 4 | Юго-Восточный административный округ | 6.07 |
| 5 | Западный административный округ | 5.56 |
| 6 | Восточный административный округ | 5.15 |
| 7 | Северо-Западный административный округ | 4.38 |
Вывод:
Посмотрим на распределения рейтингов заведений по категориям заведений.
# готовим данные для графиков
df = data.copy()
df['median_rat_categ'] = data.groupby('category')['rating'].transform('median')
df['mean_rat_categ'] = data.groupby('category')['rating'].transform('mean')
df = df.sort_values(by='median_rat_categ')
# задаем размер подложки
fig = plt.subplots(figsize=(15,5))
# строим боскплоты рейтингов
ax1 = plt.subplot(1, 2, 1)
ax1 = sns.boxplot(x='category', y='rating', data=df, showfliers=False)
plt.ylim(3.2, 5.1)
ax1.set_xticklabels(ax1.get_xticklabels(),rotation = 90)
ax1.set_title('Распределения рейтингов заведений по категориям заведений')
ax1.set_xlabel('Категории заведений')
ax1.set_ylabel('Значение рейтинга')
# строим линейный график значений среднего и медианного рейтинга
sns.set_style('darkgrid')
ax2 = plt.subplot(1, 2, 2)
ax2 = sns.lineplot(x='category', y='median_rat_categ', data=df, label='Медиана', marker='o')
ax2 = sns.lineplot(x='category', y='mean_rat_categ', data=df, label='Среднее', marker='o')
ax2.set_xticklabels(ax1.get_xticklabels(), rotation=90)
ax2.set_title('Средний и медианный рейтинги по категориям заведений')
ax2.set_xlabel('Категории заведений')
ax2.set_ylabel('Значение рейтиния')
# выводим график
plt.show()
# вернем стиль графиков к исходному
sns.set_style('dark')
Вывод:
Построим фоновую картограмму со средним рейтингом заведений каждого района.
# считаем для каждого округа средний рейтинг заведений
df = (data.groupby('district', as_index=False)['rating'].agg('mean').round(2)
.rename(columns={'rating': 'rating_mean'}))
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='rating_mean',
legend_name='Средний рейтинг заведений')
| district | rating_mean | |
|---|---|---|
| 0 | Центральный административный округ | 4.38 |
| 1 | Северный административный округ | 4.24 |
| 2 | Северо-Западный административный округ | 4.21 |
| 3 | Западный административный округ | 4.18 |
| 4 | Южный административный округ | 4.18 |
| 5 | Восточный административный округ | 4.17 |
| 6 | Юго-Западный административный округ | 4.17 |
| 7 | Северо-Восточный административный округ | 4.15 |
| 8 | Юго-Восточный административный округ | 4.10 |
Дополнительно построим фоновую картограмму с медианным рейтингом заведений каждого района.
# считаем для каждого округа медианный рейтинг заведений
df = (data.groupby('district', as_index=False)['rating'].agg('median').round(2)
.rename(columns={'rating': 'rating_mean'}))
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='rating_mean',
legend_name='Средний рейтинг заведений')
| district | rating_mean | |
|---|---|---|
| 0 | Центральный административный округ | 4.4 |
| 1 | Восточный административный округ | 4.3 |
| 2 | Западный административный округ | 4.3 |
| 3 | Северный административный округ | 4.3 |
| 4 | Северо-Западный административный округ | 4.3 |
| 5 | Юго-Западный административный округ | 4.3 |
| 6 | Южный административный округ | 4.3 |
| 7 | Северо-Восточный административный округ | 4.2 |
| 8 | Юго-Восточный административный округ | 4.2 |
Вывод:
Отобразим все заведения на карте с помощью кластеров.
# готовим данные
df = pd.DataFrame(data['district'].unique()).rename(columns={0: 'district'})
# наносим на карту кластеры и границы округов
map_chor(df_in=df,
column_1='district',
column_2='buf',
legend_name='',
clusters_on=True,
fill_opacity=0)
Вывод:
Найдем топ-15 улиц по количеству заведений и посмотрим на распределение категорий заведений по этим улицам.
# строим столбчатую диаграмму
# распределения числа заведений по улицам
# готовим данные для графика
df = (data.groupby('street', as_index = False)['name'].count()
.rename(columns={'name': 'points_count'})
.sort_values(by='points_count', ascending=False).head(15)
.sort_values(by='points_count'))
# строим график
fig = px.bar(df,
y='street',
x='points_count',
text='points_count')
# настраиваем параметры графика
fig.update_layout(title={'text': 'ТОП-15 улиц по количеству заведений','x': 0.55},
xaxis_title='Количество заведений',
yaxis_title='Название улицы',
width=950,
height=600)
fig.update_xaxes(tickvals=[])
# выводим график
fig.show()
# строим столбчатую диаграмму
# процентного распределения категорий заведений по топ-15 улиц
# готовим данные для графика
top_street = (data.groupby('street')['name'].count()
.sort_values(ascending=False).head(15).index)
df = data[data['street'].isin(top_street)]
df = (df.groupby(['category','street'], as_index = False)['name'].count()
.rename(columns={'name': 'points_count_categ_street'}))
df['all_points_street'] = df.groupby('street')['points_count_categ_street'].transform(sum)
df['perc_categ'] = df['points_count_categ_street'] / df['all_points_street'] * 100
df['all_points_categ'] = df.groupby('category')['points_count_categ_street'].transform(sum)
df = df.sort_values(by='all_points_categ')
# строим график
fig = px.bar(df,
x='perc_categ',
y='street',
color='category',
text = round(df['perc_categ']))
# настраиваем параметры графика
fig.update_layout(title={'text': 'Соотношение количества заведений на ТОП-15 улицах по категориям','x': 0.5},
legend_title='Категория заведения',
xaxis_title='Процент',
yaxis_title='Название улицы',
width=950,
height=600,
plot_bgcolor="white")
fig.update_traces(textposition="inside")
# выводим график
fig.show()
# строим столбчатую диаграмму
# процентного распределения количества заведений на ТОП-15 улиц по категориям заведений
# готовим данные для графика
top_street = (data.groupby('street')['name'].count()
.sort_values(ascending=False).head(15).index)
df = data[data['street'].isin(top_street)]
df = (df.groupby(['category','street'], as_index = False)['name'].count()
.rename(columns={'name': 'points_count_categ_street'}))
df['all_points_categ'] = df.groupby('category')['points_count_categ_street'].transform(sum)
df['perc_street'] = df['points_count_categ_street'] / df['all_points_categ'] * 100
df['all_points_street'] = df.groupby('street')['points_count_categ_street'].transform(sum)
df = df.sort_values(by=['all_points_street','all_points_categ'])
# расширим палитру цветов
colors = px.colors.qualitative.Plotly[0:10] + px.colors.qualitative.Set2[0:10]
# строим график
fig = px.bar(df,
x='perc_street',
y='category',
color='street',
text = round(df['perc_street']),
color_discrete_sequence=colors)
# настраиваем параметры графика
fig.update_layout(title={'text': 'Соотношение количества заведений в разных категориях по ТОП-15 улицам','x': 0.42},
legend_title='Название улицы',
xaxis_title='Процент',
yaxis_title='Название категорий',
width=950,
height=500,
plot_bgcolor="white")
fig.update_traces(textposition="inside")
# выводим график
fig.show()
# строим хитмпеп
# распределения числа заведений по улицам и категориям
# готовим данные для хитмепа
top_street = (data.groupby('street')['name'].count()
.sort_values(ascending=False).head(15).index)
df = data[data['street'].isin(top_street)]
data_pivot = df.pivot_table(index = 'street', columns = 'category',
values = 'name', aggfunc = 'count')
data_pivot['sum_all_1'] = data_pivot.sum(axis=1)
data_pivot=data_pivot.sort_values(by='sum_all_1',ascending=False)
data_pivot=data_pivot.drop('sum_all_1', axis=1)
data_pivot=data_pivot.T
data_pivot['sum_all_2'] = data_pivot.sum(axis=1)
data_pivot=data_pivot.sort_values(by='sum_all_2')
data_pivot=data_pivot.drop('sum_all_2', axis=1)
data_pivot=data_pivot.T
# строим хитмеп
plt.figure(figsize=(10, 7))
ax = sns.heatmap(
data_pivot,
annot=True,
fmt='.0f',
cmap= 'coolwarm',
linecolor='black',
linewidths=2)
# настраиваем параметры хитмепа
ax.tick_params(axis='x', labelsize=9)
ax.set_facecolor('#464544')
plt.xlabel('x_label')
ax.xaxis.set_ticks_position('top')
plt.title('Количество заведений по категориям и ТОП-15 улицам\n')
plt.xlabel('')
plt.ylabel('')
# выводим хитмеп
plt.show()
Вывод:
Посмотрим на улицы, на которых находится только один объект общепита.
# готовим данные для графика
df = (data.groupby('street', as_index = False)['name'].count()
.rename(columns={'name': 'points_in_street'}))
list_street = df[df['points_in_street'] == 1]['street'].unique()
df = (data[data['street'].isin(list_street)].groupby('category', as_index = False)['name'].count()
.rename(columns={'name': 'points_in_street'})
.sort_values(by='points_in_street', ascending=False))
# строим график
fig = px.bar(df,
y='category',
x='points_in_street',
color='category',
text='points_in_street')
# настраиваем параметры графика
fig.update_layout(title={'text': 'Количество "заведений-одиночек" по категориям','x': 0.5},
legend_title='Категория заведения',
xaxis_title='Количество заведений',
yaxis_title='Категория заведения',
width=850,
height=500)
fig.update_xaxes(tickvals=[])
# выводим график
fig.show()
Вывод:
Построим фоновую картограмму медианного значения среднего счета по районам. Лучше брать медиану, а не среднее, так как в ценах есть выбросы.
# считаем для каждого округа средний рейтинг заведений
df = (data.groupby('district', as_index=False)['middle_avg_bill'].median()
.rename(columns={'middle_avg_bill': 'median_avg_bill'}))
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='median_avg_bill',
legend_name='Медианный чек')
| district | median_avg_bill | |
|---|---|---|
| 0 | Западный административный округ | 1000.0 |
| 1 | Центральный административный округ | 1000.0 |
| 2 | Северо-Западный административный округ | 700.0 |
| 3 | Северный административный округ | 650.0 |
| 4 | Юго-Западный административный округ | 600.0 |
| 5 | Восточный административный округ | 575.0 |
| 6 | Северо-Восточный административный округ | 500.0 |
| 7 | Южный административный округ | 500.0 |
| 8 | Юго-Восточный административный округ | 450.0 |
Выводы:
Попробуем более подробно оценить ключевые параметры по выбору места и типа заведения и сделать итоговую оценку на основе общего влияния данных параметров.
Открывать новое заведение стоит там, где меньше конкуренция.
На конкуренцию влияют следующие параметры (чем ниже любой из этих параметров - тем ниже конкуренция):
Построим два хитмепа отнормированных параметров по районам и категориям заведений. Эти графики понадобятся для общей оценки параметров конкуренции по всем заведениям в районах (т.е. для сравнения районов по количеству непрямых конкурентов) и категориях (для сравнений категорий по общему количеству прямых конкурентов по всем районам). Под нормировкой в данном случае понимается, что значения по каждому параметру берутся относительно минимального значения параметра.
# функция для построения хитмепа отнормированных значений параметров по районам
# для выбора района
def heatmap_dstr(data_in, column, title):
# считаем количество заведений по районам
df1 = (data_in.groupby(column, as_index = False)['name'].count()
.rename(columns={'name': 'count'}))
# считаем средний рейтинг заведений по районам
df2 = (data_in.groupby(column, as_index=False)['rating']
.agg('mean').rename(columns={'rating': 'rating_mean'}))
# считаем число заведений на 1 кв км по районам
df3 = data_in.groupby(['dstr','area'], as_index = False)['name'].count()
df3['count_per_area'] = df3['name'] / df3['area']
df3=df3[['dstr','count_per_area']]
# объединяем все параметры в одну таблицу
df_vs = (df1
.merge(df2, on=column)
.merge(df3, on=column))
# помещаем названия районов в индексы
df_vs = df_vs.set_index(column)
# нормируем показатели каждого параметра
for column in df_vs.columns:
df_vs[column] = ((df_vs[column] - df_vs[column].min())
/ (df_vs[column].min()))
# переименуем столбцы
df_vs=df_vs.rename(columns={'rating_mean': 'Средний рейтинг',
'count_per_area': 'Число заведений на 1 кв. км.',
'count': 'Число заведений'})
# сортируем данные
df_vs['mean_all_1'] = df_vs.mean(axis=1)
df_vs=df_vs.sort_values(by='mean_all_1')
df_vs=df_vs.drop('mean_all_1', axis=1)
df_vs=df_vs.T
df_vs['mean_all_2'] = df_vs.mean(axis=1)
df_vs=df_vs.sort_values(by='mean_all_2',ascending=False)
df_vs=df_vs.drop('mean_all_2', axis=1)
# строим хитмеп параметров
plt.figure(figsize=(9, 2))
ax = sns.heatmap(
df_vs,
annot=True,
cmap= 'coolwarm',
linecolor='black',
linewidths=2,
fmt='.2f',
vmin=0,
vmax=1)
# настраиваем параметры хитмепа
ax.xaxis.set_ticks_position('top')
plt.title(title)
plt.xlabel('')
plt.ylabel('')
# выводим хитмеп
plt.show()
# функция для построения хитмепа отнормированных значений параметров по категориям
# для выбора категории
def heatmap_categ(data_in, column, title):
# считаем количество заведений по категориям заведений
df1 = (data_in.groupby(column, as_index = False)['name'].count()
.rename(columns={'name': 'count'}))
# считаем средний рейтинг заведений по категориям заведений
df2 = (data_in.groupby(column, as_index=False)['rating']
.agg('mean').rename(columns={'rating': 'rating_mean'}))
# объединяем все параметры в одну таблицу
df_vs = (df1
.merge(df2, on=column))
# помещаем названия районов в индексы
df_vs = df_vs.set_index(column)
# нормируем показатели каждого параметра
for column in df_vs.columns:
df_vs[column] = ((df_vs[column] - df_vs[column].min())
/ (df_vs[column].min()))
# переименуем столбцы
df_vs=df_vs.rename(columns={'rating_mean': 'Средний рейтинг',
'count': 'Число заведений'})
# сортируем данные
df_vs['mean_all_1'] = df_vs.mean(axis=1)
df_vs=df_vs.sort_values(by='mean_all_1')
df_vs=df_vs.drop('mean_all_1', axis=1)
df_vs=df_vs.T
df_vs['mean_all_2'] = df_vs.mean(axis=1)
df_vs=df_vs.sort_values(by='mean_all_2',ascending=False)
df_vs=df_vs.drop('mean_all_2', axis=1)
# строим хитмеп параметров
plt.figure(figsize=(9, 1.5))
ax = sns.heatmap(
df_vs,
annot=True,
cmap= 'coolwarm',
linecolor='black',
linewidths=2,
fmt='.2f',
vmin=0,
vmax=1)
# настраиваем параметры хитмепа
ax.tick_params(axis='x', labelsize=9)
ax.xaxis.set_ticks_position('top')
plt.title(title)
plt.xlabel('')
plt.ylabel('')
# выводим хитмеп
plt.show()
Пояснение к хитмепам, построенным ниже:
# строим хитмеп по районам
heatmap_dstr(data_in=data,
column='dstr',
title='Значения параметров конкуренции по районам\n')
Вывод:
# строим хитмеп по типам заведений
heatmap_categ(data_in=data,
column='category',
title='Значения параметров конкуренции по типам заведений\n')
Вывод:
Детализируем выбор типа и района для нового заведения. Построим хитмепы для уникальных сочетаний типа заведения и района по абсолютному числу заведений, числу заведений на единицу площади и среднему рейтингу. Они понадобятся для сравнительной оценки параметров конкуренции по прямым конкурентам (т.е. для сравнения параметров одних и тех же типов заведений в одном и том же районе). Это поможет понять заведения каких типов в каких районах открывать лучше всего.
# функция для построения хитмепа значений по уникальным сочетаниям
# района и категории заведения
def heatmap_norm(data_in, # передаем датафрейм
column1, # по этому столбцу собираем районы
column2, # по этому столбцу собираем категории
values, # по этому столбцу получаем значения
aggfunc, # функция для получения значений
title, # задаем заголовок графику
values_2=0, # доп. значения для доп. расчетов
aggfunc_2=0, # доп. функция для дополнительных расчетов
fmt='.2f', # задаем округление значений
vmax=1 # устанавливает верхнюю границу покраски
):
# собираем сводную таблицу
data_pivot = data_in.pivot_table(index = column1, columns = column2,
values = values, aggfunc = aggfunc)
# эта часть выполняется, когда нужно посчитать число заведений на ед. площади
if values_2 != 0:
data_pivot_2 = data_in.pivot_table(index = column1, columns = column2,
values = values_2, aggfunc = aggfunc_2)
data_pivot = data_pivot / data_pivot_2
# находим ячейки с минимальным значением
# и выводим в каких уникальных значениях района и категории находятся мин. значение
min_value = data_pivot.min().min()
contains_value = data_pivot.eq(min_value).any()
columns_with_value_1 = contains_value[contains_value == True].index.to_list()
contains_value = data_pivot.eq(min_value).any(axis=1)
columns_with_value_2 = contains_value[contains_value == True].index.to_list()
for i in range(len(columns_with_value_1)):
print('Мин. значение {:.2f} (не отнормированное) для {} в {}'.format(min_value,
columns_with_value_1[i],
columns_with_value_2[i]))
# нормируем показатели каждого параметра
# относительно минимального значения по всему датафрейму
for column in data_pivot.columns:
data_pivot[column] = ((data_pivot[column] - min_value)
/ (min_value))
# сортируем данные
data_pivot['mean_all_1'] = data_pivot.mean(axis=1)
data_pivot=data_pivot.sort_values(by='mean_all_1')
data_pivot=data_pivot.drop('mean_all_1', axis=1)
data_pivot=data_pivot.T
data_pivot['mean_all_2'] = data_pivot.mean(axis=1)
data_pivot=data_pivot.sort_values(by='mean_all_2',ascending=False)
data_pivot=data_pivot.drop('mean_all_2', axis=1)
# строим хитмеп
plt.figure(figsize=(10, 4.5))
ax = sns.heatmap(
data_pivot,
annot=True,
fmt=fmt,
cmap= 'coolwarm',
linecolor='black',
linewidths=2,
vmax=vmax)
# настраиваем параметры хитмепа
ax.tick_params(axis='x', labelsize=9)
plt.xlabel('x_label')
ax.xaxis.set_ticks_position('top')
plt.title(title)
plt.xlabel('')
plt.ylabel('')
# выводим хитмеп
plt.show()
# возвращаем таблицу, по которой строился хитмеп
return data_pivot.T
Пояснение к хитмепам, построенным ниже:
Посмотрим на распределение количества заведений по категориям и районам.
# строим хитмеп значений количества заведений в районах и категориях
df_chois_dstr_1 = heatmap_norm(data_in = data,
column1 = 'dstr',
column2 = 'category',
values = 'name',
aggfunc = 'count',
title = 'Количество заведений по категориям и районам\n',
fmt='.2f')
Мин. значение 12.00 (не отнормированное) для булочная в СЗАО
Вывод:
# строим хитмеп значений количества заведений на 1 кв.км. в районах и категориях
df_chois_dstr_2 = heatmap_norm(data_in = data,
column1 = 'dstr',
column2 = 'category',
values = 'name',
aggfunc = 'count',
values_2 = 'area',
aggfunc_2 = 'mean',
title = 'Количество заведений на 1 кв. км. по категориям и районам \n')
Мин. значение 0.11 (не отнормированное) для булочная в ЮВАО
Вывод:
# строим хитмеп значений среднего рейтинга заведений в районах и категориях
df_chois_dstr_3 = heatmap_norm(data_in = data,
column1 = 'dstr',
column2 = 'category',
values = 'rating',
aggfunc = 'mean',
title = 'Средний рейтинг по категориям и районам\n')
Мин. значение 3.93 (не отнормированное) для быстр.пит. в ЮВАО
Вывод:
Итоговый вывод:
Лучше всего открыть булочную в СЗАО.
Причины:
Общие выводы по итогам анализа рынка заведений общепита в Москве:
Если нашей целью является выбор наиболее предпочительного типа и расположения нового заведения, то можно дать следующие рекомендации:
Общие рекомендации по типу заведения
Общие рекомендации по расположению заведения (округ)
Общие рекомендации по расположению заведения (улица)
Посмотрим, сколько всего у нас кофеен
print('Всего кофеен: {}'.format(data[data['category'] == 'кофейня']['name'].count()))
Всего кофеен: 1413
Посмотрим распределение числа кофеен по районам
# готовим данные для графика
df=(data[data['category'] == 'кофейня'].groupby('dstr',as_index = False)['name'].count()
.rename(columns={'name': 'count_coff'})
.sort_values(by='count_coff'))
print(df)
# строим график
fig = px.bar(df,
y='dstr',
x='count_coff',
text='count_coff')
# настраиваем параметры графика
fig.update_layout(title={'text': 'Количество кофеен по районам','x': 0.5},
xaxis_title='Число кофеен',
yaxis_title='Район',
width=800,
height=600)
fig.update_xaxes(tickvals=[])
# выводим график
fig.show()
dstr count_coff 4 СЗАО 62 7 ЮВАО 89 8 ЮЗАО 96 0 ВАО 105 6 ЮАО 131 1 ЗАО 150 3 СВАО 159 2 САО 193 5 ЦАО 428
Выводы:
Посмотрим распределение доли кофеен относительно числа заведений всех типов в разбивке по районам
# готовим данные для графиков
df = data.copy()
df['all_count_dstr'] = df.groupby('district')['name'].transform('count')
df = (df[df['category'] == 'кофейня'].groupby(['district','all_count_dstr'], as_index = False)['name'].count()
.rename(columns={'name': 'count_coff'}))
df['perc_coff'] = round(df['count_coff'] / df['all_count_dstr'] * 100,2)
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='perc_coff',
legend_name='Процент кофеен')
| district | perc_coff | |
|---|---|---|
| 0 | Северный административный округ | 21.44 |
| 1 | Центральный административный округ | 19.09 |
| 2 | Северо-Восточный административный округ | 17.85 |
| 3 | Западный административный округ | 17.63 |
| 4 | Северо-Западный административный округ | 15.16 |
| 5 | Южный административный округ | 14.69 |
| 6 | Юго-Западный административный округ | 13.54 |
| 7 | Восточный административный округ | 13.16 |
| 8 | Юго-Восточный административный округ | 12.46 |
Выводы:
Теперь посмотрим на число кафеен на 1 квадратных километр площади по районам.
# готовим данные для графиков
df = data[data['category'] == 'кофейня'].groupby(['district','area'],as_index = False)['name'].count()
# считаем число заведений на 1 кв.км.
df['count_per_area'] = round(df['name'] / df['area'],2)
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='count_per_area',
legend_name='Число кафеен на 1 кв.км.')
| district | count_per_area | |
|---|---|---|
| 0 | Центральный административный округ | 6.47 |
| 1 | Северный административный округ | 1.70 |
| 2 | Северо-Восточный административный округ | 1.56 |
| 3 | Южный административный округ | 0.99 |
| 4 | Западный административный округ | 0.98 |
| 5 | Юго-Западный административный округ | 0.86 |
| 6 | Юго-Восточный административный округ | 0.76 |
| 7 | Восточный административный округ | 0.68 |
| 8 | Северо-Западный административный округ | 0.66 |
Вывод:
Чтобы визуально лучше сравнить разницу в значениях числа кофеен на 1 кв.км по районам посмотрим еще на распределение числа кофеен на 1 кв.км. без ЦАО.
# готовим данные
df = data[data['category'] == 'кофейня'].groupby(['district','area'],as_index = False)['name'].count()
# считаем число заведений на 1 кв.км.
df['count_coff_per_area'] = round(df['name'] / df['area'],2)
# удаляем ЦАО
df = df[df['district'] != 'Центральный административный округ']
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='count_coff_per_area',
legend_name='Число кафеен на 1 кв.км.',
nan_fill_opacity=0.2)
| district | count_coff_per_area | |
|---|---|---|
| 0 | Северный административный округ | 1.70 |
| 1 | Северо-Восточный административный округ | 1.56 |
| 2 | Южный административный округ | 0.99 |
| 3 | Западный административный округ | 0.98 |
| 4 | Юго-Западный административный округ | 0.86 |
| 5 | Юго-Восточный административный округ | 0.76 |
| 6 | Восточный административный округ | 0.68 |
| 7 | Северо-Западный административный округ | 0.66 |
Вывод:
Посмотрим сколько всего круглосуточных кофеен и их процент относительно всех кофеен.
all_coff = data[data['category'] == 'кофейня']['name'].count()
coff_24hour = data[(data['category'] == 'кофейня') & (data['is_24/7'] == True)]['name'].count()
print('Круглосуточных кофеен {}, что составляет {:.1%} от всех кофеен.'.format(coff_24hour,coff_24hour/all_coff))
Круглосуточных кофеен 59, что составляет 4.2% от всех кофеен.
Посмотрим, где располагаются круглосуточные кофейни
# готовим данные для маркеров круглосуточных кофеен
df = data[(data['category'] == 'кофейня') & data['is_24/7']]
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='buf',
legend_name='',
markers_on=True,
fill_opacity=0,
zoom_start=11)
Вывод:
Посмотрим как распределяются средние рейтинги кофеен по районам
# готовимы данные для отображения
df = (data[data['category'] == 'кофейня'].groupby('district', as_index=False)['rating']
.agg('mean').round(2).rename(columns={'rating': 'rating_mean_coff'}))
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='rating_mean_coff',
legend_name='Средний рейтинг кофеен')
| district | rating_mean_coff | |
|---|---|---|
| 0 | Центральный административный округ | 4.34 |
| 1 | Северо-Западный административный округ | 4.33 |
| 2 | Северный административный округ | 4.29 |
| 3 | Восточный административный округ | 4.28 |
| 4 | Юго-Западный административный округ | 4.28 |
| 5 | Юго-Восточный административный округ | 4.23 |
| 6 | Южный административный округ | 4.23 |
| 7 | Северо-Восточный административный округ | 4.22 |
| 8 | Западный административный округ | 4.20 |
Выводы:
Посмотрим на медианную стоимость чаши капучино по районам. Лучше брать медиану, а не среднее, так как в ценах есть выбросы.
# готовим данные
df = (data[data['category'] == 'кофейня'].groupby('district', as_index=False)['middle_coffee_cup'].agg('median')
.rename(columns={'middle_coffee_cup': 'median_coffee_cup'}))
# строим хороплет
map_chor(df_in=df,
column_1='district',
column_2='median_coffee_cup',
legend_name='Медианная стоимость чашки капучино')
| district | median_coffee_cup | |
|---|---|---|
| 0 | Юго-Западный административный округ | 198.0 |
| 1 | Центральный административный округ | 190.0 |
| 2 | Западный административный округ | 189.0 |
| 3 | Северо-Западный административный округ | 165.0 |
| 4 | Северо-Восточный административный округ | 162.5 |
| 5 | Северный административный округ | 159.0 |
| 6 | Южный административный округ | 150.0 |
| 7 | Юго-Восточный административный округ | 147.5 |
| 8 | Восточный административный округ | 135.0 |
Вывод:
Открывать кофейню стоит там, где меньше конкуренция.
На конкуренцию влияют следующие параметры (чем ниже любой из этих параметров - тем ниже конкуренция):
Для более детальной оценки суммарного влияния данных параметров на выбор района для открытия кофейни, построим хитмеп отнормированных параметров конкуренции по каждому району (нормировка - относительно минимального значения параметра). Добавим к основным параметрам еще число круглосуточных кофеен, чтобы оценить относительную разницу в их количестве по районам.
Пояснение к хитмепу, построенному ниже:
# считаем количество кофеен по районам
df1 = (data[data['category'] == 'кофейня'].groupby('dstr',as_index = False)['name'].count()
.rename(columns={'name': 'count_coff'}))
# считаем число кофеен на 1 кв км по районам
df2 = data[data['category'] == 'кофейня'].groupby(['dstr','area'], as_index = False)['name'].count()
df2['count_coff_per_area'] = df2['name'] / df2['area']
df2=df2[['dstr','count_coff_per_area']]
# считаем средний рейтинг кофеен по районам
df3 = (data[data['category'] == 'кофейня'].groupby('dstr', as_index=False)['rating']
.agg('mean').round(2).rename(columns={'rating': 'rating_mean_coff'}))
# считаем сколько кофеен 24/7 по районам
df4 = (data[(data['category'] == 'кофейня') & (data['is_24/7'])].groupby('dstr', as_index=False)['name'].count()
.rename(columns={'name': 'count_coff_24/7'}))
# считаем количество всех заведений по районам
df5 = (data.groupby('dstr',as_index = False)['name'].count()
.rename(columns={'name': 'count_all'}))
# считаем число заведений на 1 кв км по районам
df6 = data.groupby(['dstr','area'], as_index = False)['name'].count()
df6['count_all_per_area'] = df6['name'] / df6['area']
df6=df6[['dstr','count_all_per_area']]
# считаем средний рейтинг кофеен по районам
df7 = (data.groupby('dstr', as_index=False)['rating']
.agg('mean').round(2).rename(columns={'rating': 'rating_mean_all'}))
# объединяем все параметры в одну таблицу
df_vs = (df1
.merge(df2, on='dstr')
.merge(df3, on='dstr')
.merge(df4, on='dstr')
.merge(df5, on='dstr')
.merge(df6, on='dstr')
.merge(df7, on='dstr')
)
# помещаем названия районов в индексы
df_vs = df_vs.set_index('dstr')
# нормируем показатели каждого параметра
for column in df_vs.columns:
df_vs[column] = ((df_vs[column] - df_vs[column].min())
/ (df_vs[column].min()))
# переименуем столбцы для отображения
df_vs=df_vs.rename(columns={'count_coff': 'Число кофеен',
'count_coff_per_area': 'Число кофеен на 1 кв. км.',
'rating_mean_coff': 'Средний рейтинг кофеен',
'rating_mean_all': 'Средний рейтинг всех заведений',
'count_all_per_area': 'Число всех заведений на 1 кв.км.',
'count_all': 'Число всех заведений',
'count_coff_24/7': 'Число кофеен 24/7'})
# сортируем данные
df_vs['mean_all_1'] = df_vs.mean(axis=1)
df_vs=df_vs.sort_values(by='mean_all_1')
df_vs=df_vs.drop('mean_all_1', axis=1)
df_vs=df_vs.T
df_vs['mean_all_2'] = df_vs.mean(axis=1)
df_vs=df_vs.sort_values(by='mean_all_2',ascending=False)
df_vs=df_vs.drop('mean_all_2', axis=1)
# строим хитмеп параметров
plt.figure(figsize=(9, 5))
ax = sns.heatmap(
df_vs,
annot=True,
cmap= 'coolwarm',
linecolor='black',
linewidths=2,
fmt='.2f',
vmax=1)
# настраиваем параметры хитмепа
ax.xaxis.set_ticks_position('top')
plt.title('Значения параметров конкуренции для кофеен по районам\n')
plt.xlabel('')
plt.ylabel('')
# выводим хитмеп
plt.show()
Вывод:
Меньше всего конкурентов для кофеен по:
Итоговый вывод по выбору места для открытия кофейни:
Рекомендуется открыть круглосуточную кофейню в СЗАО.
При открытии стоит ориентироваться на стоимость чаши капучино ~165 руб
Причины: